﻿#region

using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using MediaSage.Lib.Configuration;
using MediaSage.Lib.Database;

#endregion

namespace MediaSage.Lib.Plugin
{
    public class PluginManager
    {
        #region Fields

        private readonly Config config;
        private readonly IDictionary<string, AppDomain> domains;
        private readonly IDictionary<string, PluginBase> loadedPlugins;
        private readonly IDictionary<string, string> nameLocationPair;
        private readonly Logger logger;

        #endregion Fields

        #region Constructors

        public PluginManager(Config Config)
        {
            config = Config;
            logger = Config.Logger;
            loadedPlugins = new Dictionary<string, PluginBase>();
            domains = new Dictionary<string, AppDomain>();
            nameLocationPair = new Dictionary<string, string>();

            logger.Log("PluginManager Initialized");
        }

        #endregion Constructors

        #region Methods

        public PluginBase GetPluginByName(string name)
        {
            if (nameLocationPair.ContainsKey(name))
            {
                if (loadedPlugins.ContainsKey(nameLocationPair[name]))
                {
                    return loadedPlugins[nameLocationPair[name]];
                }
            }
            return null;
        }

        public bool PluginLoaded(PluginEntity pe)
        {
            logger.Log("Entering PluginLoaded(" + pe.Location + ")");
            if (loadedPlugins.ContainsKey(pe.Location))
            {
                logger.Log("Exiting PluginLoaded(" + pe.Location + "), rc==true");
                return true;
            }

            logger.Log("Exiting PluginLoaded(" + pe.Location + "), rc==false");
            return false;
        }

        public PluginBase GetPlugin(string PluginName)
        {
            logger.Log(String.Format("Entering PluginManager.GetPlugin({0})", PluginName));
            foreach (PluginEntity pe in config.Plugins)
            {
                if (pe.Name.Equals(PluginName))
                {
                    PluginBase pie = GetPlugin(pe);
                    logger.Log("Exiting PluginManager.GetPlugin({0})");
                    return pie;
                }
            }

            logger.Log(Logger.Levels.WARN, String.Format("Plugin: {0} not configured", PluginName));
            logger.Log(String.Format("Exiting PluginManager.GetPlugin({0}), rc==null", PluginName));
            return null;
        }

        public PluginBase GetPlugin(PluginEntity pe)
        {
            logger.Log("Entering PluginManager.GetPlugin(" + pe.Location + ")");
            if (loadedPlugins.ContainsKey(pe.Location))
            {
                logger.Log("Plugin currently loaded, exiting with that plugin");
                return loadedPlugins[pe.Location];
            }

            if (!domains.ContainsKey(pe.Location))
            {
                var ads = new AppDomainSetup();
                ads.ApplicationBase = Path.GetDirectoryName(pe.Location);
                ads.DisallowBindingRedirects = false;
                ads.DisallowCodeDownload = true;

                domains.Add(pe.Location, AppDomain.CreateDomain(pe.Location, null, ads));
            }

            var loader =
                (RemoteLoader)
                domains[pe.Location].CreateInstanceFromAndUnwrap("MediaSageLib.dll", "MediaSage.Lib.Plugin.RemoteLoader");

            Assembly plugin = null;
            try
            {
                plugin = loader.Load(Path.GetFileNameWithoutExtension(pe.Location));

                //plugin = loader.Load(System.IO.Path.Combine(System.IO.Path.GetDirectoryName(pe.Location),System.IO.Path.GetFileNameWithoutExtension(pe.Location)));
            }
            catch (Exception e)
            {
                logger.Log("Exception loading plugin: " + e.Message);
                logger.Log("Exiting GetPlugin(" + pe.Location + "), rc==null");
                return null;
            }
            PluginBase b = null;
            foreach (Type type in plugin.GetTypes())
            {
                if (type.IsSubclassOf(typeof(PluginBase)) && type.IsAbstract == false)
                {
                    try
                    {
                        b = (PluginBase)Activator.CreateInstance(type, config, pe);
                    }
                    catch (Exception e)
                    {
                        logger.Log(Logger.Levels.ERROR, String.Format("Exception loading plugin: {0}, Message: {1}", pe.Location, e.Message));
                        logger.Log("Exiting GetPlugin(" + pe.Location + "), rc==null");
                        return null;
                    }
                }
            }

            if (b != null)
            {
                loadedPlugins.Add(pe.Location, b);
                if (pe.Name == null)
                {
                    pe.Name = b.GetVersionInfo().Name;
                }
                if (!nameLocationPair.ContainsKey(pe.Name))
                {
                    nameLocationPair.Add(pe.Name, pe.Location);
                }
            }

            logger.Log("Exiting GetPlugin(" + pe.Location + "), plugin loaded successfully");
            return b;
        }

        private bool isFileLocked(IOException exception)
        {
            var errorCode = Marshal.GetHRForException(exception) & ((1 << 16) - 1);
            return errorCode == 32 || errorCode == 33;
        }

        public void UnloadPlugin(PluginEntity pe)
        {
            logger.Log("Entering UnloadPlugin(" + pe.Location + ")");
            if (loadedPlugins.ContainsKey(pe.Location))
            {
                if (domains.ContainsKey(pe.Location))
                {
                    AppDomain.Unload(domains[pe.Location]);
                    loadedPlugins.Remove(pe.Location);
                    logger.Log(String.Format("Unloaded plugin @{0}", pe.Location));

                    // Check to see if the file is locked, if it is, sleep a bit and try up to 4 times
                    var attempts = 0;
                    bool lockFree = false;
                    while (true)
                    {
                        try
                        {
                            var test = File.Open(pe.Location, FileMode.Open, FileAccess.Read, FileShare.None);
                            if (test != null)
                            {
                                lockFree = true;
                                test.Close();
                                logger.Log(String.Format("No lock on {0}", pe.Location));
                                logger.Log("Exiting UnloadPlugin(" + pe.Location + ")");
                                return;
                            }
                        }
                        catch (IOException E)
                        {
                            if (!isFileLocked(E))
                            {
                                logger.Log(Logger.Levels.ERROR,
                                           String.Format("Exception unlocking {0}, msg: {1}", pe.Location, E.Message));
                                logger.Log("Exiting UnloadPlugin(" + pe.Location + ")");
                                return;
                            }
                            if (++attempts > 10)
                            {
                                logger.Log(Logger.Levels.ERROR,
                                           String.Format("Couldnt unlock {0}, reload may fail", pe.Location));
                                return;
                            }
                        }
                        catch (Exception E)
                        {
                            logger.Log(Logger.Levels.ERROR,
                                       String.Format("Exception unlocking {0}, msg: {1}", pe.Location, E.Message));
                            logger.Log("Exiting UnloadPlugin(" + pe.Location + ")");
                            return;
                        }
                        Thread.Sleep(500);
                    }
                }
            }
            logger.Log("Exiting UnloadPlugin(" + pe.Location + ")");
        }

        #endregion Methods
    }

    public class RemoteLoader : MarshalByRefObject
    {
        #region Methods

        public Assembly Load(string themeName)
        {
            return AppDomain.CurrentDomain.Load(themeName);
        }

        #endregion Methods
    }
}